/*****************************************************************
 *   Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 ****************************************************************/
package org.epiphron.wam;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.servlet.FilterChain;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang.StringUtils;
import org.epiphron.wam.CallGraph.MethodNode;
import org.semanticweb.owlapi.apibinding.OWLManager;
import org.semanticweb.owlapi.model.IRI;
import org.semanticweb.owlapi.model.OWLClass;
import org.semanticweb.owlapi.model.OWLDataFactory;
import org.semanticweb.owlapi.model.OWLDataProperty;
import org.semanticweb.owlapi.model.OWLDataPropertyAssertionAxiom;
import org.semanticweb.owlapi.model.OWLNamedIndividual;
import org.semanticweb.owlapi.model.OWLObjectProperty;
import org.semanticweb.owlapi.model.OWLOntology;
import org.semanticweb.owlapi.model.OWLOntologyManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * The builder of web application model
 */
public class WebAppModelBuilder {
	static final Logger logger = LoggerFactory.getLogger(WebAppModelBuilder.class);
	
	static final IMethodMatcher[] LISTENER_MATCHERS = new IMethodMatcher[] {
		new DirectMethodMatcher("requestInitialized", ServletRequestEvent.class),
		new DirectMethodMatcher("requestDestroyed", ServletRequestEvent.class),
	};
	
	static final IMethodMatcher[] FILTER_MATCHERS = new IMethodMatcher[] {
		new DirectMethodMatcher("doFilter", ServletRequest.class, ServletResponse.class, FilterChain.class)
	};
	
	static final IMethodMatcher[] SERVLET_MATCHERS = new IMethodMatcher[] {
		new DirectMethodMatcher("doGet", HttpServletRequest.class, HttpServletResponse.class),
		new DirectMethodMatcher("doPost", HttpServletRequest.class, HttpServletResponse.class),
		new DirectMethodMatcher("doPut", HttpServletRequest.class, HttpServletResponse.class),
		new DirectMethodMatcher("doDelete", HttpServletRequest.class, HttpServletResponse.class),
		new DirectMethodMatcher("doTrace", HttpServletRequest.class, HttpServletResponse.class),
		new DirectMethodMatcher("doOptions", HttpServletRequest.class, HttpServletResponse.class),
		new DirectMethodMatcher("doHead", HttpServletRequest.class, HttpServletResponse.class)
	};
	
	final OWLDataFactory dataFactory = OWLManager.getOWLDataFactory();

	private OWLObjectProperty containsProp;
	private OWLDataProperty nameProp;
	private OWLDataProperty descProp;
	private OWLDataProperty classProp;
	private OWLDataProperty paramProp;
	private OWLDataProperty lineNumberProp;
	private OWLDataProperty methodNameProp;
	private OWLDataProperty methodSignatureProp;
	private OWLObjectProperty performsProp;

	private XPath xpath;

	private OWLOntologyManager manager;
	
	private int opIndex;
	
	private CallGraph<OWLNamedIndividual> callGraph;
	
	public WebAppModelBuilder() {
		containsProp = WAMUtilites.getObjectProperty("contains");
		nameProp = WAMUtilites.getDataProperty("name");
		descProp = WAMUtilites.getDataProperty("description");
		classProp = WAMUtilites.getDataProperty("class");
		paramProp = WAMUtilites.getDataProperty("param");
		lineNumberProp = WAMUtilites.getDataProperty("lineNumber");
		performsProp = WAMUtilites.getObjectProperty("performs");
		methodNameProp = WAMUtilites.getDataProperty("methodName");
		methodSignatureProp = WAMUtilites.getDataProperty("methodSignature");
		
		xpath = XPathFactory.newInstance().newXPath();
		manager = OWLManager.createOWLOntologyManager();
		
		callGraph = new CallGraph<OWLNamedIndividual>();
	}
	
	/**
	 * Popupates WAM schema ontology with data
	 * @param wam
	 * @throws Exception
	 */
	public OWLOntology buildWebAppModel(OWLOntology wamSchema) throws Exception {
		// parse web.xml
		InputStream webXmlStream = ClassLoader.getSystemResourceAsStream("WEB-INF/web.xml");
		if (webXmlStream == null) {
			throw new WAMException("No WEB-INF/web.xml found on the classpath");
		}
		
		DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		Document webXml = builder.parse(webXmlStream);
				
		Object root = xpath.evaluate("web-app", webXml, XPathConstants.NODE);
		String displayName = xpath.evaluate("display-name/text()", root);
		String description = xpath.evaluate("description/text()", root);
		
		logger.info("Found webapp " + displayName);
		
		String appNS = "http://" + displayName + "/"; // TODO infer namespace
		
		// create ontology with inferred ID
		
		OWLOntology wam = manager.createOntology(wamSchema.getAxioms(), wamSchema.getOntologyID().getOntologyIRI());//n IRI.create(appNS));
		
		OWLClass webAppClass = WAMUtilites.getClass("WebApplication");
		OWLNamedIndividual webApp = getIndividual(appNS, displayName);
		
		manager.addAxiom(wam, dataFactory.getOWLClassAssertionAxiom(webAppClass, webApp));
		manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(nameProp, webApp, displayName));
		manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(descProp, webApp, description.trim()));
				
		Collection<OWLNamedIndividual> servlets = extractHandlers(wam, appNS, root, webApp, "Servlet");
		Collection<OWLNamedIndividual> filters = extractHandlers(wam, appNS, root, webApp, "Filter");
		Collection<OWLNamedIndividual> listeners = extractListeners(wam, appNS, root, webApp);
		
		extractMethods(wam, servlets, SERVLET_MATCHERS, HttpServlet.class, appNS);
		extractMethods(wam, filters, FILTER_MATCHERS, null, appNS);
		extractMethods(wam, listeners, LISTENER_MATCHERS, null, appNS);
		
		return wam;
	}

	/**
	 * Extracts handlers (servlets or filters) from web.xml
	 */
	private Collection<OWLNamedIndividual> extractHandlers(OWLOntology wam,
			String appNS, 
			Object root, 
			OWLNamedIndividual webApp,
			String className) throws XPathExpressionException {
		List<OWLNamedIndividual> extracted = new ArrayList<OWLNamedIndividual>();
		
		// loading handlers
		OWLClass handlerClass = WAMUtilites.getClass(className);
		String tagName = className.toLowerCase();
		
		NodeList nl = (NodeList) xpath.evaluate(tagName, root, XPathConstants.NODESET);
		for (int i = 0; i < nl.getLength(); i++) {
			Node handlerNode = nl.item(i);
			
			String name = xpath.evaluate(tagName + "-name/text()", handlerNode).trim();
			String dispName = xpath.evaluate("display-name/text()", handlerNode).trim();
			String clazz = xpath.evaluate(tagName + "-class/text()", handlerNode).trim();
			
			OWLNamedIndividual handler = getIndividual(appNS, tagName + "/" + name);
			
			manager.addAxiom(wam, dataFactory.getOWLClassAssertionAxiom(handlerClass, handler));
			manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(nameProp, handler, name));
			manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(descProp, handler, dispName));
			manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(classProp, handler, clazz));
			
			manager.addAxiom(wam, dataFactory.getOWLObjectPropertyAssertionAxiom(containsProp, webApp, handler));
			
			logger.info("Found " + tagName + " " + dispName);
			extracted.add(handler);
		}
		
		// mappings
		nl = (NodeList) xpath.evaluate(tagName + "-mapping", root, XPathConstants.NODESET);
		OWLDataProperty mappingProp = WAMUtilites.getDataProperty("mapUrl");
		for (int i = 0; i < nl.getLength(); i++) {
			Node handlerNode = nl.item(i);
			
			String name = xpath.evaluate(tagName + "-name/text()", handlerNode).trim();
			String mapping = xpath.evaluate("url-pattern/text()", handlerNode).trim();
			
			logger.info("Found mapping " + mapping + " for " + name);
			
			OWLNamedIndividual servlet = getIndividual(appNS, tagName + "/" + name);
			manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(mappingProp, servlet, mapping));
		}
		
		return extracted;
	}
	
	/**
	 * Extracts listeners from web.xml
	 */
	private Collection<OWLNamedIndividual> extractListeners(OWLOntology wam,
			String appNS, 
			Object root, 
			OWLNamedIndividual webApp) throws XPathExpressionException {
		List<OWLNamedIndividual> extracted = new ArrayList<OWLNamedIndividual>();
		
		OWLClass listenerClass = WAMUtilites.getClass("Listener");
		
		NodeList nl = (NodeList) xpath.evaluate("listener", root, XPathConstants.NODESET);
		for (int i = 0; i < nl.getLength(); i++) {
			Node listenerNode = nl.item(i);
			
			String dispName = xpath.evaluate("display-name/text()", listenerNode).trim();
			String clazz = xpath.evaluate("listener-class/text()", listenerNode).trim();
			
			String name = "".equals(dispName) ? clazz : dispName;
			OWLNamedIndividual listener = getIndividual(appNS, "listener/" + name);
			
			logger.info("Found listener " + name);
			
			manager.addAxiom(wam, dataFactory.getOWLClassAssertionAxiom(listenerClass, listener));
			
			if (!"".equals(dispName)) {
				manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(descProp, listener, dispName));
			}
			manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(classProp, listener, clazz));
			manager.addAxiom(wam, dataFactory.getOWLObjectPropertyAssertionAxiom(containsProp, webApp, listener));
			
			extracted.add(listener);
		}
		
		return extracted;
	}
	
	/**
	 * Extracts methods for given collection of handlers
	 * @param handlers handlers
	 * @param methodMatchers matchers for methods that should be extracted 
	 * @param boundingClass the class in the hierarchy that makes algorithm stop. If <code>null</code>, 
	 *   java.lang.Object will be used
	 * @return
	 */
	private Collection<OWLNamedIndividual> extractMethods(OWLOntology wam,
			Collection<OWLNamedIndividual> handlers,
			IMethodMatcher[] methodMatchers, Class<?> boundingClass,
			String appNS) {
		OWLClass methodClass = WAMUtilites.getClass("Method");
		OWLObjectProperty definesProp = WAMUtilites.getObjectProperty("defines");
		
		List<OWLNamedIndividual> extracted = new ArrayList<OWLNamedIndividual>();
		
		for (OWLNamedIndividual handler : handlers) {
			String clazzName = null;
			Set<OWLDataPropertyAssertionAxiom> axioms = wam.getDataPropertyAssertionAxioms(handler);
			for (OWLDataPropertyAssertionAxiom axiom : axioms) {
				if (axiom.getProperty().equals(classProp)) {
					clazzName = axiom.getObject().getLiteral();
					break;
				}
			}
			
			if (!StringUtils.isEmpty(clazzName)) {
				try {
					List<IMethodMatcher> matchers = new ArrayList<IMethodMatcher>(Arrays.asList(methodMatchers));
					for (Class<?> clazz = Class.forName(clazzName); 
							clazz != boundingClass && clazz != Object.class && matchers.size() > 0;
							clazz = clazz.getSuperclass()) {
						for (Iterator<IMethodMatcher> matcherIt = matchers.iterator(); matcherIt.hasNext();) {
							IMethodMatcher matcher = matcherIt.next();
							
							for (Method method : clazz.getDeclaredMethods()) {
								if (matcher.match(method)) {
									logger.info("Found web method " + method.getName() + " for handler " + handler.getIRI());
									
									OWLNamedIndividual methodInd = dataFactory.getOWLNamedIndividual(
										IRI.create(handler.getIRI() + "/" + method.getName()));
									manager.addAxiom(wam, 
										dataFactory.getOWLClassAssertionAxiom(methodClass, methodInd));
									manager.addAxiom(wam, 
										dataFactory.getOWLDataPropertyAssertionAxiom(nameProp, methodInd, method.getName()));
									manager.addAxiom(wam, 
										dataFactory.getOWLObjectPropertyAssertionAxiom(definesProp, handler, methodInd));
									
									try {
										crawl(wam, methodInd, method, appNS);
									}
									catch (Exception ex) {
										logger.warn("Exception crawling method " +
											clazz.getName() + "." + method.getName() + "()", ex);
									}
									extracted.add(methodInd);
									
									matcherIt.remove();
									break;
								}
							}
						}
					}
				} catch (Exception e) {
					logger.warn("Could not extract methods for " +
						handler.getIRI() + ". Skipping..", e);
				}
			}
			else {
				logger.warn("Class attribute not found for " + handler.getIRI());
			}
		}
		
		return extracted;
	}
	
	/**
	 * Crawls method's code for known operations 
	 */
	private void crawl(final OWLOntology wam, final OWLNamedIndividual methodInd,
			Method method, final String appNS) throws ClassNotFoundException {
		logger.debug("Crawling from method " + method.getDeclaringClass() + "." + method.getName() + "..");
		
		// crawl the callgraph and build all individuals for operations (so far, unlinked to method)
		Collection<MethodNode<OWLNamedIndividual>> nodes = callGraph.buildCalledMethods(method, new IOperationFactory<OWLNamedIndividual>() {
			public OWLNamedIndividual buildOperation(
					IOperationDescription opDesc, String clazz, String method, 
					String signature, int line, Object param) {
				if (opDesc.getName().equals("AddCookieOperation")) {
					opDesc.getName();
				}
				
				OWLClass opClass = dataFactory.getOWLClass(IRI.create(WAMUtilites.NS + opDesc.getName()));
				
				String NSbase = appNS + clazz + "/" + method + "/op";
				//int i = 1;
				//for(; wam.containsIndividualInSignature(IRI.create(NSbase + i)); i++);
				OWLNamedIndividual opInd = dataFactory.getOWLNamedIndividual(IRI.create(NSbase + ++opIndex));
									
				manager.addAxiom(wam, dataFactory.getOWLClassAssertionAxiom(opClass, opInd));
				
				if (param != null) {
					manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(paramProp, opInd, param.toString()));
				}
				
				if (line != -1) {
					manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(lineNumberProp, opInd, line));
				}
				
				manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(classProp, opInd, clazz));
				manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(methodNameProp, opInd, method));
				manager.addAxiom(wam, dataFactory.getOWLDataPropertyAssertionAxiom(methodSignatureProp, opInd, signature));
				
				return opInd;
			}
		});
		
		// now iterate through all nodes and link all operations to current method
		for (MethodNode<OWLNamedIndividual> node : nodes) {
			for (OWLNamedIndividual op : node.getOperations()) {
				manager.addAxiom(wam, dataFactory.getOWLObjectPropertyAssertionAxiom(performsProp, methodInd, op));
			}
		}
	}
	
	OWLNamedIndividual getIndividual(String ns, String name) {
		return dataFactory.getOWLNamedIndividual(IRI.create(ns + name));
	}
	
	public static void main(String[] args) throws Exception {
		if (args.length != 1) {
			System.err.println("Usage: WebAppModelBuilder <output_file_name>");
			return;
		}
		String file = args[0];
		
		OWLOntologyManager manager = OWLManager.createOWLOntologyManager();
		OWLOntology wamSchema = manager.loadOntologyFromOntologyDocument(
			ClassLoader.getSystemResourceAsStream("org/epiphron/wam/epiphron-wam.owl"));
		
		OWLOntology wam = new WebAppModelBuilder().buildWebAppModel(wamSchema);
		
		FileOutputStream fout = new FileOutputStream(file);
		try {
			manager.saveOntology(wam, fout);
		}
		finally {
			fout.close();
		}
	}
}
